<?PHP
/*
 * $Id: barge-logging.classes.php 139 2009-12-16 15:29:56Z namba0219 $
 * =============================================================================
 * Barge - web application framework on PHP5
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * =============================================================================
 * For more information, see http://barge.googlecode.com/
 */
namespace barge\logging;

use \ReflectionClass;
use \Exception;

use barge\System;
use barge\utils\ArrayList;
use barge\utils\Config;
use barge\utils\String;
use barge\utils\HashMap;
use barge\utils\ObjectUtils;

/**
 * Logger factory class like a log4j.
 * <p/>
 * For exsample.
 * <pre>
 * $log = LogFactory::getLogger();
 * $log = LogFactory::getLogger('CustomLogger');
 * $log = LogFactory::getLogger(__NAMESPACE__);
 * $log = LogFactory::getLogger(__CLASS__);
 * </pre>
 *
 * @version 1.0 2008/08/13
 * @author Kenji Namba
 * @since barge 1.0 2008/08/13
 */
class LogFactory
{
	const ROOT_LOGGER = "root";

	/**
	 * @var HashMap
	 */
	private static $loggers;

	/**
	 * ロガーを取得する
	 * <p/>
	 * ロガー名を指定することで、名前付きロガーを取得することができます。ロガー名の検索は、
	 * 前方一致で検索されます。指定されたロガーが設定されていない場合このメソッドは、
	 * 何もしないロガーを返します。よってこのメソッドは、何らかのロガーを必ず返します。また
	 * 一度生成されたロガーはキャッシュされ、２回目以降はキャッシュされたロガーを返します。
	 *
	 * @param string $name ロガー名、省略された場合はルートロガーが使用されます
	 * @return Logger
	 * @since 1.0 2006/06/20
	 */
	public static function getLogger($name = self::ROOT_LOGGER)
	{
		if (self::$loggers === null) {
			self::$loggers = new HashMap();
		}
		if (! self::$loggers->contains($name)) {
			$logger = null;
			$appendar = null;
			/* @var $logconfig HashMap */
			$configs = System::getProperty('Logging');
			if ($configs !== null) {
				/* @var $config Config */
				$config = null;
				foreach ($configs as $key => $temp) {
					if (substr($name, 0, strlen($key)) === $key) {
						$config = $temp;
						break;
					}
				}
				if ($config !== null) {
					$properties = $config->getProperty('appenderProperties', new Config());
					$appendarName = $config->getProperty('appenderClass');
					$priority = $config->getProperty('priority');
					if ($appendarName !== null) {
						$reflectionClass = new ReflectionClass($appendarName);
						$appendar = $reflectionClass->newInstance($properties);
						$logger = new Logger($priority, $appendar);
					}
				}
			}
			if ($logger === null) {
				$logger = new Logger('', null);
			}
			self::$loggers->put($name, $logger);
		}

		return self::$loggers->get($name);
	}
}

/**
 * ログアペンダー
 * <p/>
 * ログアペンダーは、実際のログ出力処理のインターフェースです。
 *
 * @version 1.0 2006/08/13
 * @author Kenji Namba
 * @since Barge 1.0 2006/08/13
 */
interface LogAppender
{
	/**
	 * @param Config $properties アペンダープロパティー
	 */
	public function __construct(Config $properties);

	/**
	 * ログ出力処理
	 *
	 * @param string $var メッセージ
	 * @param string $level ログレベル
	 * @since 1.0 2006/08/13
	 */
	public function append($var, $level);
}

/**
 * ロガー
 * @version 1.0 2006/08/13
 * @author Kenji Namba
 * @since Barge 1.0 2006/08/13
 */
class Logger
{
	/**
	 * @var bool
	 * @since 1.0 2006/10/18
	 */
	protected $infoEnabled = false;

	/**
	 * @var bool
	 * @since 1.0 2006/10/18
	 */
	protected $fatalEnabled = false;

	/**
	 * @var bool
	 * @since 1.0 2006/10/18
	 */
	protected $errorEnabled = false;

	/**
	 * @var bool
	 * @since 1.0 2006/10/18
	 */
	protected $debugEnabled = false;

	/**
	 * @var bool
	 * @since 1.0 2006/10/18
	 */
	protected $warnEnabled = false;

	/**
	 * @var LogAppender
	 */
	protected $appender;

	/**
	 * コンストラクタ
	 *
	 * @param string $level
	 * @param LogAppender $appender[optional] = null
	 */
	public function __construct($level, LogAppender $appender = null)
	{
		if ($appender !== null) {
			$this->appender = $appender;
			switch ($level) {
				case 'debug' :
					$this->debugEnabled = true;
				case 'debug' :
				case 'info' :
					$this->infoEnabled = true;
				case 'debug' :
				case 'info' :
				case 'warn' :
					$this->warnEnabled = true;
				case 'debug' :
				case 'info' :
				case 'warn' :
				case 'error' :
					$this->errorEnabled = true;
				case 'debug' :
				case 'info' :
				case 'warn' :
				case 'error' :
				case 'fatal' :
					$this->fatalEnabled = true;
			}
		}
	}

	/**
	 * INFO レベルのログ出力が有効かどうかを返します
	 * @return boolean
	 */
	public function isInfoEnabled()
	{
		return $this->infoEnabled;
	}

	/**
	 * DEBUGレベルのログ出力が有効かどうかを返します
	 * @return boolean
	 */
	public function isDebugEnabled()
	{
		return $this->debugEnabled;
	}

	/**
	 * ERRORレベルのログ出力が有効かどうかを返します
	 * @return boolean
	 */
	public function isErrorEnabled()
	{
		return $this->errorEnabled;
	}

	/**
	 * FATALレベルのログ出力が有効かどうかを返します
	 * @return bool
	 */
	public function isFatalEnabled()
	{
		return $this->fatalEnabled;
	}

	/**
	 * WARNレベルのログ出力が有効かどうかを返します
	 * @return bool
	 */
	public function isWarnEnabled()
	{
		return $this->warnEnabled;
	}

	/**
	 * INFO レベルのログ出力
	 * @param mixed var
	 */
	public function info($var)
	{
		if ($this->isInfoEnabled())
		$this->appender->append($var, 'info');
	}

	/**
	 * WARN レベルのログ出力
	 * @param mixed var
	 */
	public function warn($var)
	{
		if ($this->isWarnEnabled())
		$this->appender->append($var, 'warn');
	}

	/**
	 * DEBUG レベルのログ出力
	 * @param mixed $var
	 */
	public function debug($var)
	{
		if ($this->isDebugEnabled())
		$this->appender->append($var, 'debug');
	}

	/**
	 * ERRRO レベルのログ出力
	 * @param mixed $var
	 */
	public function error($var)
	{
		if ($this->isErrorEnabled())
		$this->appender->append($var, 'error');
	}

	/**
	 * FATAL レベルのログ出力
	 * @param mixed $var
	 */
	public function fatal($var)
	{
		if ($this->isFatalEnabled())
		$this->appender->append($var, 'fatal');
	}
}

/**
 * 複数のロガーをチェインさせるためのロガー
 * @version 1.0
 * @see LogAppender
 * @author Kenji Namba
 * @since Barge 1.2 2008/08/13
 */
class ChainAppender implements LogAppender
{
	/**
	 * @var ArrayList
	 */
	private $appenders;

	/**
	 * Constractor.
	 *
	 * @param HashMap $config
	 * @see LogAppender#__construct()
	 */
	public function __construct(Config $config)
	{
		$appenderList = $config->getProperty('appenders');
		$list = split(',', $appenderList);

		$this->appenders = new ArrayList();
		foreach ($list as $name) {
			$config = System::getProperty('Logging', $name);
			$properties = $config->getProperty('appenderProperties') ?: new Config();
			$appendarName = $config->getProperty('appenderClass');
			if ($appendarName !== null) {
				$reflectionClass = new ReflectionClass($appendarName);
				$appendar = $reflectionClass->newInstance($properties);
				$this->appenders->add($appendar);
			}
		}
	}

	/**
	 * @param mixed $var
	 * @param string $level レベル文字列
	 * @since 1.0 2006/08/13
	 */
	public function append($var, $level)
	{
		foreach ($this->appenders as $appender)
		$appender->append($var, $level);
	}
}

/**
 * 複数のロガーをチェインさせるためのロガー
 * @version 1.0
 * @see LogAppender
 * @author Kenji Namba
 * @since Barge 1.2 2008/08/13
 */
class ConsoleAppender implements LogAppender
{
	const DEFAULT_FORMAT = "{timestamp} {level} - {message}\n";

	private static $posixExists = false;

	/**
	 * @var string
	 */
	private $format;

	/**
	 * @var string
	 */
	private $encoding;

	/**
	 * Constractor.
	 *
	 * @param Config $config
	 * @see LogAppender#__construct()
	 */
	public function __construct(Config $config)
	{
		self::$posixExists = function_exists('posix_getpid');
		$this->format =	System::firstNotEmpty($config->getProperty('format'), self::DEFAULT_FORMAT);
		$this->encoding = $config->getProperty('encoding');
	}

	/**
	 * @param mixed $var
	 * @param string $level レベル文字列
	 * @since 1.0 2006/08/13
	 */
	public function append($var, $level)
	{
		System::out()->write(
			String::format($this->format,
			array(
				'level' => strtoupper($level),
				'timestamp' => date("Y/m/d H:i:s"),
				'message' => (string)$var,
				'pid' => (self::$posixExists ? posix_getpid() : ''),
				'lf' => "\n",
		)));
	}
}

/**
 * 単一のファイルにログを出力するためのアペンダー
 * <p/>
 * プロパティーに最大サイズが指定された場合、出力されたログが最大サイズを超えた場合、ファイル
 * をトランケートします。最大サイズが指定されていない場合、無限にファイルに追記します。
 *
 * @version 1.0
 * @see LogAppender
 * @author Kenji Namba
 * @since Barge 1.0 2006/08/13
 */
class FileAppender implements LogAppender
{
	const DEFAULT_FORMAT = "{timestamp} {level} - {message}\n";

	/**
	 * @var bool
	 */
	private static $posixExists = false;

	/**
	 * @var string
	 */
	protected $filepath;

	/**
	 * @var stream
	 * @since 1.2 2008/09/27
	 */
	protected $fp = false;

	/**
	 * @var long
	 * @since 1.2 2008/09/27
	 */
	protected $fsize = 0;

	/**
	 * @var string
	 */
	protected $format;

	/**
	 * @var int
	 */
	protected $maxbytes;

	/**
	 * @var bool
	 */
	protected $append = true;

	/**
	 *
	 * @param Config $config
	 * @see LogAppender#__construct()
	 */
	public function __construct(Config $config)
	{
		self::$posixExists = function_exists('posix_getpid');
		$this->filepath = System::getBaseDir().'/'.$config->getProperty('filepath');
		$this->maxbytes = $config->getProperty('maxbytes');
		$this->format = System::firstNotEmpty($config->getProperty('format'), self::DEFAULT_FORMAT);
		$this->append = !($config->getProperty('append') === 'false');
	}

	/**
	 * @param mixed $var
	 * @param string $level レベル文字列
	 * @since 1.0 2006/08/13
	 */
	public function append($var, $level)
	{
		if ($this->fp === false) {
			if ($this->append) {
				$this->fp = @fopen($this->filepath, 'a');
			} else {
				$this->fp = @fopen($this->filepath, 'w');
			}
			if ($this->fp !== false) {
				$this->fsize = filesize($this->filepath);
			}
		} else if ($this->maxbytes < $this->fsize) {
			fclose($this->fp);
			$this->fp = @fopen($this->filepath, 'w');
			$this->fsize = 0;
		}

		if ($this->fp === false) {
			return;
		}

		$buff = String::format($this->format,
			array(
				'level' => strtoupper($level),
				'timestamp' => date("Y/m/d H:i:s"),
				'message' => is_scalar($var) ? (string)$var : ObjectUtils::toString($var),
				'pid' => (self::$posixExists ? posix_getpid() : ''),
				'lf' => "\n",
			));
		$this->fsize += fwrite($this->fp, $buff);
	}

	/**
	 * @since 1.2 2008/09/27
	 */
	public function __destruct()
	{
		if ($this->fp !== false) {
			fclose($this->fp);
			$this->fp = false;
		}
	}
}


/**
 *
 * @version 1.0
 * @see LogAppender
 * @author Kenji Namba
 * @since barge 1.2 2008/08/13
 */
class FirePHPAppender implements LogAppender
{
	private static $posixExists = false;

	/**
	 * @var string
	 */
	protected $format;

	/**
	 * @var FirePHP
	 */
	protected $firePHP;

	/**
	 * Constractor.
	 *
	 * @param Config $config
	 * @see LogAppender#__construct()
	 */
	public function __construct(Config $config)
	{
		$this->firePHP = \FirePHP::getInstance(true);
	}

	/**
	 * @param mixed $message メッセージ
	 * @param string $level レベル文字列
	 * @since 1.0 2006/08/13
	 */
	public function append($message, $level)
	{
		$fblv = null;
		if ($level === 'warn') {
			$fblv = \FirePHP::WARN;
		} else if ($level === 'info' || $level === 'debug') {
			$fblv = \FirePHP::INFO;
		} else if ($level === 'fatal' || $level === 'error') {
			$fblv = \FirePHP::ERROR;
		}
		$this->firePHP->fb($message, $fblv);
	}
}


/**
 * 日付ごとにログをローテーションする
 *
 * @version 1.0
 * @see FileAppender
 * @author Kenji Namba
 * @since Barge 1.0 2006/06/20
 */
class DailyFileAppender extends FileAppender
{
	const DEFAULT_DATE_PATTERN = 'Y-m-d';

	/**
	 * @param Config $properties
	 * @since 1.0 2006/06/20
	 */
	public function __construct(Config $properties)
	{
		parent::__construct($properties);
		$datePattern = $properties->getProperty('datePattern');
		if (empty($datePattern)) {
			$datePattern = self::DEFAULT_DATE_PATTERN;
		}
		$this->filepath .= '_' . date($datePattern);
	}
}

/**
 * メール送信
 *
 * @version 1.0 2009/11/15
 * @see LogAppender
 * @author Kenji Namba
 * @since barge 1.4 2009/11/15
 */
class MailAppender implements LogAppender
{
	const DEFAULT_FORMAT = "{timestamp} {level} - {message}\n";

	private static $posixExists = false;

	/**
	 * @var string
	 */
	private $format;

	/**
	 * @var string
	 */
	private $encoding;

	/**
	 * Constractor.
	 *
	 * @param Config $config
	 * @see LogAppender#__construct()
	 */
	public function __construct(Config $config)
	{
		self::$posixExists = function_exists('posix_getpid');
		$this->format = System::firstNotEmpty($config->getProperty('format'), self::DEFAULT_FORMAT);
		$this->encoding = $config->getProperty('encoding');
	}

	/**
	 * @param mixed $var
	 * @param string $level レベル文字列
	 * @since 1.0 2006/08/13
	 */
	public function append($var, $level)
	{
//		System::registerShutdownHandler()
		System::out()->write(
			String::format($this->format,
			array(
				'level' => strtoupper($level),
				'timestamp' => date("Y/m/d H:i:s"),
				'message' => is_scalar($var) ? (string)$var : ObjectUtils::toString($var),
				'pid' => (self::$posixExists ? posix_getpid() : ''),
				'lf' => "\n",
		)));
	}
}
